package com.spzx.channel.service.impl;

import com.spzx.channel.service.ItemService;
import com.spzx.common.core.domain.R;
import com.spzx.common.core.exception.ServiceException;
import com.spzx.common.redis.util.RedisLockUtil;
import com.spzx.product.api.RemoteProductService;
import com.spzx.product.domain.vo.ItemVO;
import io.netty.util.internal.ObjectUtil;
import jakarta.annotation.Resource;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Service
public class ItemServiceImpl implements ItemService {
    @Resource
    RemoteProductService remoteProductService;
    //装配的是 spzx-common-redis RedisConfig中初始化的对象，配置了键和值的序列化器
    // 使用redisTemplate存储的值，会通过FastJson2转为json字符串存入到redis中
    @Resource
    RedisTemplate redisTemplate;
    @Resource
    StringRedisTemplate stringRedisTemplate;

    /*
        openfeign拦截器：
            可以将feign远程访问的请求拦截 修改请求报文或者参数

        可以将业务流程总结：
            查询商品详情： 数据一致性问题(在product中更新商品信息时)
                1、bitmap解决缓存穿透问题：
                        前置操作：预加载skuId到redis的bitmap中
                2、查询缓存
                3、缓存命中失败，获取分布式锁(使用skuId拼接锁的key)  缓存穿透雪崩击穿
                    - 设置唯一uuid避免误删除
                    - 设置过期时间避免死锁
                    - 看门狗线程自动续期避免锁失效
                4、再次查询缓存
                5、远程调用查询数据库
                6、存到缓存中：
                    - 空数据设置较短过期时间
                7、释放分布式锁
     */
    //根据skuId查询商品详情业务
    @Override
    public ItemVO selectItemBySkuId(Long skuId) {
        //判断查询的skuId是不是数据库存在的sku的id值，如果是 可以继续执行，如果不是，不需要执行
        //= 1、商品服务启动时，需要加载所有的skuId存到redis的bitmap中
        //= 2、查询业务中 通过bitmap判断skuId是否存在
        Boolean exists = stringRedisTemplate.opsForValue().getBit("spzx:product:skuIds", skuId);
        if(!exists){
            //查询的skuId sku表中不存在
            throw new ServiceException(skuId+" 的商品数据不存在!");
        }

        //1、查询是否存在缓存  如果存在直接返回
        String key = "spzx:channel:item:" + skuId;
        Object cache = redisTemplate.opsForValue().get(key);
        if (!ObjectUtils.isEmpty(cache)) {
            return (ItemVO) cache;
        }
        //2、查询缓存失败：远程调用查询数据库
        //！！！优化： 查询数据库之前 ，需要获取分布式锁 ，查询skuId=2 和skuId=3的请求 需要获取不同的分布式锁
        //     使用锁的目的是为了保证 一批操作相同数据的请求 只有一个查询数据库 其它的阻塞等待查询缓存
        String lockKey = "spzx:channel:lock:"+skuId;
        String uuid = UUID.randomUUID().toString().replace("-","");
        RedisLockUtil.lock(stringRedisTemplate,lockKey ,uuid , 10);
        //线程获取到锁以后 再次查询缓存，避免自己获取锁失败自旋时，前面的线程已经查询到数据并设置到缓存中了
        try {
            cache = redisTemplate.opsForValue().get(key);
            if (!ObjectUtils.isEmpty(cache)) {
                return (ItemVO) cache;
            }

            //商品数据都存在product库中 需要 spzx-product服务查询
            //只能等 product服务暴露一个接口 远程调用查询得到商品详情
            R<ItemVO> itemVOR = remoteProductService.getItemVOBySkuId(skuId);
            if (itemVOR.getCode() != 200) {
                //远程调用失败
                throw new ServiceException(itemVOR.getMsg());
            }

            //3、缓存数据
            //redis最好设置过期时间：如果没有设置，并且redis配置了默认的rdb持久化，以后这些数据一直存在redis中
            //空数据可以设置较短的过期时间 缓存数据设置较长的过期时间
            redisTemplate.opsForValue().set(key, itemVOR.getData() ,
                    itemVOR.getData()==null?60:3600
                    , TimeUnit.SECONDS);

            //4、释放分布式锁

            //返回查询到的结果
            return itemVOR.getData();
        } finally {
            //在finally中释放锁：避免业务出现运行时异常 导致锁未释放
            RedisLockUtil.unlock(stringRedisTemplate,lockKey,uuid);
        }
    }


    @Override
    public void testLock() {
        //== 1、获取分布式锁 setnx  设置10秒的过期时间
        String uuid = UUID.randomUUID().toString().replace("-", "");
        String lockKey = "spzx:channel:test:lock";
        int timeout = 10;
        //使用工具类获取分布式锁
        RedisLockUtil.lock(stringRedisTemplate, lockKey, uuid, timeout);
        try {
            //== 2、获取分布式锁成功 执行number+1业务
            //1、获取redis中的number
            Object obj = redisTemplate.opsForValue().get("number");
            if (obj == null) {
                //初始化redis中的数据
                redisTemplate.opsForValue().set("number", 0);
                obj = 0;
            }
            //2、执行number+1
            Integer number = Integer.parseInt(obj.toString());
            //                int i = 1/0;
            //            TimeUnit.SECONDS.sleep(1000);
            //3、更新到redis中
            redisTemplate.opsForValue().set("number", number + 1);
            //调用工具类释放锁
            RedisLockUtil.unlock(stringRedisTemplate,lockKey,uuid);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }
}